package org.jspar.io;

import java.io.IOException;
import java.io.Reader;
import java.util.HashMap;
import java.util.Map;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.jspar.Constants;
import org.jspar.model.Direction;
import org.jspar.model.Module;
import org.jspar.model.ModuleModel;
import org.jspar.model.Net;
import org.jspar.model.NetList;
import org.jspar.model.Terminal;
import org.jspar.model.TerminalModel;
import org.jspar.model.Utility;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

public class NetListReader {

	public NetList read(Reader reader) throws IOException {
		try {
			DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
			Document doc = builder.parse(new InputSource(reader));
			return makeNetList(doc.getDocumentElement());
		} catch (ParserConfigurationException e) {
			throw new IOException(e);
		} catch (SAXException e) {
			throw new IOException(e);
		}
	}

	private NetList makeNetList(Element documentElement) throws IOException {
		Map<String, ModuleModel> registry = makeModuleTypeRegistry(
				documentElement.getElementsByTagName("module-type"));
		NetList netlist = new NetList();
		NodeList l = documentElement.getElementsByTagName("module");
		for (int i = 0; i < l.getLength(); ++i) {
			Module m = makeModule((Element)l.item(i), registry);
			if (m.type().isSystemTerminal())
				netlist.addExternal(m.terminals()[0], m);
			netlist.addModule(m);
		}
		l = documentElement.getElementsByTagName("net");
		for (int i = 0; i < l.getLength(); ++i) {
			netlist.addNet(makeNet((Element)l.item(i), netlist));
		}
		return netlist;
	}

	private Map<String, ModuleModel> makeModuleTypeRegistry(NodeList nodeList) {
		Map<String, ModuleModel> registry = new HashMap<String, ModuleModel>();
		for (int i = 0; i < nodeList.getLength(); ++i) {
			Element e = (Element) nodeList.item(i);
			makeModuleType(e, registry);
		}
		return registry;
	}

	private void makeModuleType(Element e, Map<String, ModuleModel> registry) {
		String name = e.getAttribute("name").toUpperCase();
		boolean isSystemTerminal = Boolean.valueOf(e.getAttribute("system-terminal"));
		int width = Constants.GATE_LENGTH;
		int height = Constants.GATE_HEIGHT;
		if (isSystemTerminal) {
			width = Constants.SYSTERM_SIZE;
			height = Constants.SYSTERM_SIZE;
		}
		ModuleModel type = new ModuleModel(name, width, height);
		NodeList terminals = e.getElementsByTagName("terminal");
		for (int i = 0; i < terminals.getLength(); ++i) {
			Element t = (Element) terminals.item(i);
			type.addTerminal(makeTerminalModel(t.getAttribute("name"), i, terminals.getLength(),
					makeTerminalDirection(t.getAttribute("type")), type));
		}
		
		registry.put(type.name(), type);
	}
	
	private TerminalModel makeTerminalModel(String name, int index, int noOfTerms, Direction dir, ModuleModel model) {
		int side;
		int xPos;
		int yPos;
		switch (dir.value()) {
		case Direction.IN_VALUE:
			side = Terminal.LEFT;
			xPos = -1;
			yPos = Utility.relative_term_location(index, noOfTerms, model
					.height(), Constants.SPACES_PER_PIN);
			break;
		case Direction.OUT_VALUE:
			side = Terminal.RIGHT;
			xPos = model.width() + 1;
			yPos = Utility.relative_term_location(index, noOfTerms, model
					.height(), Constants.SPACES_PER_PIN);

			break;
		case Direction.INOUT_VALUE:
			side = Terminal.UP;
			xPos = Utility.relative_term_location(index, noOfTerms, model
					.width(), Constants.SPACES_PER_PIN);
			yPos = model.height() + 1;
			break;

		case Direction.OUTIN_VALUE:
			side = Terminal.DOWN;
			xPos = Utility.relative_term_location(index, noOfTerms, model
					.width(), Constants.SPACES_PER_PIN);
			yPos = -1;
			break;
		default:
			throw new RuntimeException(
					"add_in_out: terminal with bad direction");
		}
		return new TerminalModel(name, xPos, yPos, side, dir);
	}

	private Direction makeTerminalDirection(String dir) {
		if (dir.equals("in")) {
			return Direction.IN;
		}
		if (dir.equals("out")) {
			return Direction.OUT;
		}
		if (dir.equals("inout")) {
			return Direction.INOUT;
		}
		throw new RuntimeException();
	}

	private Module makeModule(Element e, Map<String, ModuleModel> registry) throws IOException {
		//boolean anonymousType = false;
		ModuleModel type = registry.get(e.getAttribute("type").toUpperCase());
		if (type == null) {
			throw new IOException("unknown module type '"+type+"'");
			//registry.add(type = new ModuleType(e.getAttribute("type"), false));
			//anonymousType = true;
		}
		Module module = new Module(e.getAttribute("name"), type);
		for (TerminalModel tModel : type.getTerminals()) {
			Terminal t = tModel.create(module);
			module.addTerminal(t);
			if (t.type() == Direction.OUT) {
				module.setPrimaryOut(t);
			}
		}
		return module;
	}
	
	private Net makeNet(Element e, NetList list) throws IOException {
		NodeList l = e.getElementsByTagName("terminal");
		Net net = new Net(e.getAttribute("name"));
		for (int i = 0; i < l.getLength(); ++i) {
			Element t = (Element) l.item(i);
			String moduleName = t.getAttribute("module");
			Module m = list.getModule(moduleName);
			if (m == null)
				throw new IOException("unknown module '"+moduleName+"'");
			String termName = t.getAttribute("name");
			Terminal term = m.getTerminal(termName);
			if (term == null)
				throw new IOException("unknown terminal '"+termName+"' in module '"+moduleName+"'");
			net.addTerminal(term);
		}
		return net;
	}
}
